/* 
 * DataManager.java 28.06.2011
 *  
 *  
 * Candidate: Alessandra Otterbach
 * Prometric ID: Algisina1
 * Username: Algisina
 *  
 * Sun Certified Developer for Java 2 Platform, Standard Edition Programming 
 * Assignment (CX-310-252A)
 *  
 * This class is part of the Programming Assignment of the Sun Certified 
 * Developer for Java 2 Platform, Standard Edition certification program, must 
 * not be used out of this context and must be used exclusively by Sun 
 * Microsystems, Inc. 
 */
package suncertify.db;

import java.io.EOFException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.logging.LogRecord;
import java.util.logging.Logger;

import suncertify.dao.RoomRecord;
import suncertify.db.exc.DataException;
import suncertify.db.exc.DuplicateKeyException;
import suncertify.db.exc.RecordNotFoundException;
import suncertify.db.exc.SecurityException;
import suncertify.db.file.RecordFieldReader;
import suncertify.db.search.SearchChecker;

/**
 * @author Alessandra Otterbach
 * 
 */
public class DataManager {

    private static final String DB_NAME = "db-1x1.db";

    /** Character Encoding */
    private static final String ENCODING = "US-ASCII";

    /** The value that identifies a record as being valid */
    private static final int VALID = 0;

    private Logger logger = Logger.getLogger("suncertify.db");

    // /** The bytes that store the length of each field name */
    // private static final int FIELD_NAME_BYTES = 2;
    // /** The bytes that store the fields length */
    // private static final int FIELD_LENGTH_BYTES = 2;

    private static String path = null;

    private static RandomAccessFile file = null;

    private static ReadWriteLock recNoLock = new ReentrantReadWriteLock();

    /** Map that stores all the valid records, the key is the record number */
    private Map<Integer, String[]> roomMap = new HashMap<Integer, String[]>();

    public DataManager(String dbPath) throws RecordNotFoundException,
	    IOException {
	path = dbPath;
	file = new RandomAccessFile(path, "rw");
	// Initial map fill
	readAll();
    }

    /**
     * Reads a record with the given record number, wraps it in a String[].
     * 
     * @param recNo
     * @return an array with one record of a room in it
     * @throws RecordNotFoundException
     *             is thrown if a EOFException occurs or if the deleted flag is
     *             0
     * @throws DataException
     *             is thrown if an error during the file handling occurs. The
     *             DataException is a RuntimeException, that should be handled,
     *             so it is declared in the method signature
     */
    public String[] read(int recNo) throws RecordNotFoundException,
	    DataException {
	logger.entering("DataManager", "read()", recNo);
	final byte[] input = new byte[RoomRecord.ROOM_RECORD_LENGTH];

	// Keep synchronized block as small as possible, to keep
	// the time, that the file is blocked as small as possible.
	synchronized (file) {
	    try {
		file.seek(RoomRecord.HEADER_LENGTH + (recNo - 1)
			* RoomRecord.ROOM_RECORD_LENGTH);
		file.readFully(input);
	    } catch (IOException e) {
		if (e instanceof EOFException)
		    throw new RecordNotFoundException("EOFException in read()",
			    e);
		else {
		    throw new DataException("IOException in read()", e);
		}
	    }
	}

	RecordFieldReader readRecord = new RecordFieldReader(input);
	try {
	    int deleted = readRecord.readByte();
	    String name = readRecord.read(RoomRecord.NAME_FIELD_LENGTH);
	    String location = readRecord.read(RoomRecord.LOCATION_FIELD_LENGTH);
	    String size = readRecord.read(RoomRecord.SIZE_FIELD_LENGTH);
	    String smoking = readRecord.read(RoomRecord.SMOKING_FIELD_LENGTH);
	    String rate = readRecord.read(RoomRecord.RATE_FIELD_LENGTH);
	    String date = readRecord.read(RoomRecord.DATE_FIELD_LENGTH);
	    String owner = readRecord.read(RoomRecord.OWNER_FIELD_LENGTH);

	    // System.out.println("Record Number: " + recNo);
	    // System.out.println("deleted: " + deleted);
	    // System.out.println("name: " + name);
	    // System.out.println("location: " + location);
	    // System.out.println("size: " + size);
	    // System.out.println("smoking: " + smoking);
	    // System.out.println("rate: " + rate);
	    // System.out.println("date: " + date);
	    // System.out.println("owner: " + owner);

	    String[] roomRecordResult = null;
	    if (deleted == VALID) {
		roomRecordResult = new String[7];
		roomRecordResult[0] = name;
		roomRecordResult[1] = location;
		roomRecordResult[2] = size;
		roomRecordResult[3] = smoking;
		roomRecordResult[4] = rate;
		roomRecordResult[5] = date;
		roomRecordResult[6] = owner;
	    } else {
		throw new RecordNotFoundException();
	    }

	    logger.exiting("DataManager", "read()");
	    return roomRecordResult;
	} catch (UnsupportedEncodingException exc) {
	    throw new DataException("UnsupportedEncodingException in read()",
		    exc);
	} catch (IOException e) {
	    throw new DataException("IOException in read()", e);
	}
    }

    /**
     * 
     * 
     * @param recNo
     * @param data
     * @param lockCookie
     * @throws RecordNotFoundException
     * @throws SecurityException
     */
    public void update(int recNo, String[] data) throws DataException {
	long offset = RoomRecord.HEADER_LENGTH + RoomRecord.ROOM_RECORD_LENGTH
		* (recNo - 1);
	// Save record
	persist(offset, recNo, data);
    }

    private void persist(long offset, int recNo, String[] data)
	    throws DataException {
	logger.exiting(this.getClass().getName(), "persist", data);
	System.out.println("Daten, die gespeichert werden (persist-Methode): ");
	System.out.println(data[0]);
	System.out.println(data[1]);
	System.out.println(data[2]);
	System.out.println(data[3]);
	System.out.println(data[4]);
	System.out.println(data[5]);
	System.out.println(data[6]);
	try {

	    // Keep synchronized block as small as possible, to keep
	    // the time, that the file is blocked as small as possible.
	    synchronized (file) {
		// Put pointer at right position.
		file.seek(offset);
		writeData(data, file);
	    }

	    // Map update
	    recNoLock.writeLock().lock();
	    roomMap.put(recNo, data);
	} catch (IOException e) {
	    throw new DataException("IOException in update()", e);
	} finally {
	    recNoLock.writeLock().unlock();
	}
    }

    public int create(String[] data) throws DuplicateKeyException,
	    DataException {

	// First see if there are any deleted entries, that can be used for the
	// new entry
	int recNo = checkDeletedEntries();

	try {
	    long offset;
	    // If recNo is 0, a new record entry must be put at the end of the
	    // file
	    if (recNo == 0) {
		offset = file.length();
		// recNo is number of records in file plus one.
		recNo = getRecNos() + 1;
	    } else {
		offset = RoomRecord.HEADER_LENGTH
			+ RoomRecord.ROOM_RECORD_LENGTH * (recNo - 1);
	    }

	    // Save record
	    persist(offset, recNo, data);
	} catch (IOException e) {
	    throw new DataException("IOException in create()", e);
	}

	return recNo;
    }

    /**
     * 
     * 
     * @param recNo
     * @param lockCookie
     */
    public void delete(int recNo, long lockCookie) {
	long offset = RoomRecord.HEADER_LENGTH + RoomRecord.ROOM_RECORD_LENGTH
		* (recNo - 1);
	try {
	    // Keep synchronized block as small as possible, to keep
	    // the time, that the file is blocked as small as possible.
	    synchronized (file) {
		// Put pointer at right position.
		file.seek(offset);
		file.writeByte(1);
	    }

	    // Map update
	    recNoLock.writeLock().lock();
	    roomMap.put(recNo, null);
	} catch (IOException e) {
	    throw new DataException("IOException in create()", e);
	} finally {
	    recNoLock.writeLock().unlock();
	}
    }

    public int[] find(String[] criteria) {
	int[] recNosFound;
	List<Integer> listRecNosFound = new ArrayList<Integer>();

	SearchChecker checker = new SearchChecker();

	// First check, if there is any criteria. If not, all records
	// match.
	if (checker.checkCriteriaEmpty(criteria)) {
	    listRecNosFound.addAll(roomMap.keySet());
	} else {
	    for (int i = 1; i <= roomMap.size(); i++) {

		String[] record = roomMap.get(i);

		// null would be a deleted record.
		if (record != null && checker.checkCriteria(criteria, record)) {
		    listRecNosFound.add(i);
		}
	    }
	}

	Collections.sort(listRecNosFound);
	int sizeFoundRecNos = listRecNosFound.size();
	recNosFound = new int[sizeFoundRecNos];

	for (int j = 0; j < sizeFoundRecNos; j++) {
	    recNosFound[j] = listRecNosFound.get(j);
	}

	return recNosFound;
    }

    /**
     * Returns the recNo of the first found deleted entry or 0 if there is none.
     * 
     * @return
     */
    private int checkDeletedEntries() {
	int recNo = 0;

	for (int i = 1; i <= roomMap.size(); i++) {
	    if (roomMap.get(i) == null) {
		return i;
	    }
	}

	return recNo;
    }

    private void writeData(String[] data, RandomAccessFile raf)
	    throws IOException {
	// * 1: name <br>
	// * 2: location <br>
	// * 3: size <br>
	// * 4: smoking <br>
	// * 5: rate <br>
	// * 6: date <br>
	// * 7: owner <br>

	// Deleted Flag
	raf.writeByte(0);

	// // Name
	byte[] paddedByteArray = new byte[64];
	byte[] stringByteArray = data[0].trim().getBytes(ENCODING);
	System.arraycopy(stringByteArray, 0, paddedByteArray, 0,
		stringByteArray.length);
	raf.write(paddedByteArray);
	// Location
	stringByteArray = data[1].trim().getBytes(ENCODING);
	System.arraycopy(stringByteArray, 0, paddedByteArray, 0,
		stringByteArray.length);
	raf.write(paddedByteArray);

	// Size
	paddedByteArray = new byte[4];
	stringByteArray = data[2].trim().getBytes(ENCODING);
	System.arraycopy(stringByteArray, 0, paddedByteArray, 0,
		stringByteArray.length);
	raf.write(paddedByteArray);

	// Smoking
	paddedByteArray = new byte[1];
	stringByteArray = data[3].trim().getBytes(ENCODING);
	System.arraycopy(stringByteArray, 0, paddedByteArray, 0,
		stringByteArray.length);
	raf.write(paddedByteArray);

	// Rate
	paddedByteArray = new byte[8];
	stringByteArray = data[4].trim().getBytes(ENCODING);
	System.arraycopy(stringByteArray, 0, paddedByteArray, 0,
		stringByteArray.length);
	raf.write(paddedByteArray);

	// Date
	paddedByteArray = new byte[10];
	stringByteArray = data[5].trim().getBytes(ENCODING);
	System.arraycopy(stringByteArray, 0, paddedByteArray, 0,
		stringByteArray.length);
	raf.write(paddedByteArray);

	// Owner
	paddedByteArray = new byte[8];
	stringByteArray = data[6].trim().getBytes(ENCODING);
	System.arraycopy(stringByteArray, 0, paddedByteArray, 0,
		stringByteArray.length);
	raf.write(paddedByteArray);
    }

    /**
     * Check if record exists
     * 
     * @return
     */
    public boolean checkRecordExists(int recNo) {
	if (roomMap.get(recNo) == null) {
	    return false;
	}
	return true;
    }

    // /**
    // *
    // * Store record in Map only if valid, otherwise put null into the map.
    // *
    // * @throws IOException
    // *
    // *
    // * @throws IOException
    // */
    // public void loadRecords() throws IOException {
    // int magicCookie = file.readInt();
    // RECORD_LENGTH = file.readInt() + 1;
    // short numberFields = file.readShort();
    //
    // String[] fieldNames = new String[numberFields];
    // int[] fieldLengths = new int[numberFields];
    // for (int i = 0; i < numberFields; i++) {
    // final byte nameLengthByteArray[] = new byte[FIELD_NAME_BYTES];
    // file.read(nameLengthByteArray);
    // final int nameLength = getValue(nameLengthByteArray);
    //
    // final byte[] fieldNameByteArray = new byte[nameLength];
    // file.read(fieldNameByteArray);
    // fieldNames[i] = new String(fieldNameByteArray, ENCODING);
    //
    // final byte[] fieldLength = new byte[FIELD_LENGTH_BYTES];
    // file.read(fieldLength);
    // fieldLengths[i] = getValue(fieldLength);
    // }
    //
    // //
    // // int length = raf.readInt();
    // // countBytes += 4;
    // // System.out.println("Length: " + length);
    // //
    // // short numberFields = raf.readShort();
    // // countBytes += 2;
    // // System.out.println("Number of Fields: " + numberFields);
    // //
    // // for (int i = 0; i < 7; i++) {
    // // short nameLength = raf.readShort();
    // // countBytes += 2;
    // // System.out.println("Name Length: " + nameLength);
    // // raf.skipBytes(nameLength);
    // // countBytes += nameLength;
    // // short fieldLength = raf.readShort();
    // // countBytes += 2;
    // // System.out.println("Field Length: " + fieldLength);
    // // }
    // // System.out.println(countBytes);
    //
    // HEADER_LENGTH = (int) file.getFilePointer();
    // for (int i = 1; i <= getRecNos(file); i++) {
    //
    // String[] roomRecord = new String[7];
    // // System.out.println("***************** RECORD NR. " + i
    // // + " ***********");
    //
    // // Deleted Flag
    // byte flag = file.readByte();
    // // System.out.println("Deleted Flag: " + flag);
    //
    // // Name
    // byte[] fieldLength = new byte[64];
    // file.read(fieldLength);
    // String name = new String(fieldLength, ENCODING).trim();
    // // System.out.println("Name: " + name);
    //
    // // Location
    // file.read(fieldLength);
    // String location = new String(fieldLength, ENCODING).trim();
    // // System.out.println("Location: " + location);
    //
    // // Size
    // fieldLength = new byte[4];
    // file.read(fieldLength);
    // String size = new String(fieldLength, ENCODING).trim();
    // // System.out.println("Size: " + size);
    //
    // // Smoking
    // fieldLength = new byte[1];
    // file.read(fieldLength);
    // String smoking = new String(fieldLength, ENCODING).trim();
    // // System.out.println("Smoking: " + smoking);
    //
    // // Rate
    // fieldLength = new byte[8];
    // file.read(fieldLength);
    // String rate = new String(fieldLength, ENCODING).trim();
    // // System.out.println("Rate: " + rate);
    //
    // // Date
    // fieldLength = new byte[10];
    // file.read(fieldLength);
    // String date = new String(fieldLength, ENCODING).trim();
    // // System.out.println("Date: " + date);
    //
    // // Owner
    // fieldLength = new byte[8];
    // file.read(fieldLength);
    // String owner = new String(fieldLength, ENCODING).trim();
    // // System.out.println("Owner: " + owner);
    //
    // // Only save the record, if deleted-flag is 0
    // if (flag == VALID) {
    // roomRecord[0] = name;
    // roomRecord[1] = location;
    // roomRecord[2] = size;
    // roomRecord[3] = smoking;
    // roomRecord[4] = rate;
    // roomRecord[5] = date;
    // roomRecord[6] = owner;
    //
    // roomMap.put(i, roomRecord);
    // } else {
    // roomMap.put(i, null);
    // }
    // }
    // }

    // /**
    // * Converts to <code>int</code> the content of a given <code>byte</code>
    // * array.
    // *
    // * @param byteArray
    // * The <code>byte</code> array that contains the number to be
    // * converted.
    // * @return An <code>int</code> that represents the content of the
    // * <code>byte</code> array, provided as argument.
    // */
    // private static int getValue(final byte[] byteArray) {
    // int value = 0;
    // final int byteArrayLength = byteArray.length;
    //
    // for (int i = 0; i < byteArrayLength; i++) {
    // final int shift = (byteArrayLength - 1 - i) * 8;
    // value += (byteArray[i] & 0x000000FF) << shift;
    // }
    //
    // return value;
    // }

    /**
     * Provides the number of records over all. If the record map is filled, the
     * method returns the size of the map. Otherwise the file is read and the
     * number of records in the file is returned.
     * 
     * @return number of records
     * @throws IOException
     */
    private int getRecNos() throws IOException {
	int numberRecNos;

	if (roomMap.isEmpty()) {
	    // Length of file minus Header (74) divided record length (160)
	    numberRecNos = (int) ((file.length() - RoomRecord.HEADER_LENGTH) / RoomRecord.ROOM_RECORD_LENGTH);
	} else {
	    try {
		// Lock map
		recNoLock.readLock().lock();
		numberRecNos = roomMap.size();
	    } finally {
		recNoLock.readLock().unlock();
	    }
	}

	return numberRecNos;
    }

    /**
     * Only called in the constructor of DataManager to build the map containing
     * the records. public for testing purpose.
     * 
     * @return
     * @throws RecordNotFoundException
     * @throws IOException
     */
    public List<String[]> readAll() throws IOException {
	List<String[]> allRooms = new ArrayList<String[]>();
	int recNos = getRecNos();

	// While filling the map, we do not want any other thread to change the
	// map, so we lock it.
	recNoLock.writeLock().lock();
	try {

	    for (int i = 1; i <= recNos; i++) {
		String[] record;
		try {
		    record = read(i);
		    // Fill map
		    roomMap.put(i, record);
		    allRooms.add(record);
		} catch (RecordNotFoundException e) {
		    // if record not found, fill null in map
		    roomMap.put(i, null);
		}
	    }
	    return allRooms;
	} finally {
	    recNoLock.writeLock().unlock();
	}
    }

    public static void main(String[] args) throws RecordNotFoundException,
	    IOException {
	DataManager manager = new DataManager(
		"D:\\Entwicklung\\SCJD\\Certification\\Aufgabe\\File\\db-1x1.db");
	manager.read(31);
    }
}
